/*
 * Copyright 2007 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package im.composer.media.sound.synth;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.sound.midi.Instrument;
import javax.sound.midi.Patch;
import javax.sound.midi.Soundbank;
import javax.sound.midi.SoundbankResource;

/**
 * A SoundFont 2.04 soundbank reader.
 *
 * Based on SoundFont 2.04 specification from:
 * <p>
 * http://developer.creative.com <br>
 * http://www.soundfont.com/ ;
 *
 * @author Karl Helgason
 */
public class SF2Soundbank implements Soundbank {

	// version of the Sound Font RIFF file
	protected int major = 2;
	protected int minor = 1;
	// target Sound Engine
	protected String targetEngine = "EMU8000";
	// Sound Font Bank Name
	protected String name = "untitled";
	// Sound ROM Name
	protected String romName = null;
	// Sound ROM Version
	protected int romVersionMajor = -1;
	protected int romVersionMinor = -1;
	// Date of Creation of the Bank
	protected String creationDate = null;
	// Sound Designers and Engineers for the Bank
	protected String engineers = null;
	// Product for which the Bank was intended
	protected String product = null;
	// Copyright message
	protected String copyright = null;
	// Comments
	protected String comments = null;
	// The SoundFont tools used to create and alter the bank
	protected String tools = null;
	// The Sample Data loaded from the SoundFont
	private ModelByteBuffer sampleData = null;
	private ModelByteBuffer sampleData24 = null;
	private File sampleFile = null;
	private boolean largeFormat = false;
	private List<SF2Instrument> instruments = new ArrayList<SF2Instrument>();
	private List<SF2Layer> layers = new ArrayList<SF2Layer>();
	private List<SF2Sample> samples = new ArrayList<SF2Sample>();

	public SF2Soundbank() {
	}

	public SF2Soundbank(URL url) throws IOException {

		InputStream is = url.openStream();
		try {
			readSoundbank(is);
		} finally {
			is.close();
		}
	}

	public SF2Soundbank(File file) throws IOException {
		largeFormat = true;
		sampleFile = file;
		InputStream is = new FileInputStream(file);
		try {
			readSoundbank(is);
		} finally {
			is.close();
		}
	}

	public SF2Soundbank(InputStream inputstream) throws IOException {
		readSoundbank(inputstream);
	}

	private void readSoundbank(InputStream inputstream) throws IOException {
		RIFFReader riff = new RIFFReader(inputstream);
		try {
			if (!riff.getFormat().equals("RIFF")) {
				throw new RIFFInvalidFormatException("Input stream is not a valid RIFF stream!");
			}
			if (!riff.getType().equals("sfbk")) {
				throw new RIFFInvalidFormatException("Input stream is not a valid SoundFont!");
			}
			while (riff.hasNextChunk()) {
				RIFFReader chunk = riff.nextChunk();
				if (chunk.getFormat().equals("LIST")) {
					if (chunk.getType().equals("INFO"))
						readInfoChunk(chunk);
					if (chunk.getType().equals("sdta"))
						readSdtaChunk(chunk);
					if (chunk.getType().equals("pdta"))
						readPdtaChunk(chunk);
				}
			}
		} catch (IOException e) {
			throw e;
		} catch (Exception e) {
			throw new IOException(e);
		} finally {
			try {
				riff.close();
			} catch (Exception e) {
			}
			try {
				inputstream.close();
			} catch (Exception e) {
			}
		}
	}

	private void readInfoChunk(RIFFReader riff) throws IOException {
		while (riff.hasNextChunk()) {
			RIFFReader chunk = riff.nextChunk();
			String format = chunk.getFormat();
			if (format.equals("ifil")) {
				major = chunk.readUnsignedShort();
				minor = chunk.readUnsignedShort();
			} else if (format.equals("isng")) {
				this.targetEngine = chunk.readString(chunk.available());
			} else if (format.equals("INAM")) {
				this.name = chunk.readString(chunk.available());
			} else if (format.equals("irom")) {
				this.romName = chunk.readString(chunk.available());
			} else if (format.equals("iver")) {
				romVersionMajor = chunk.readUnsignedShort();
				romVersionMinor = chunk.readUnsignedShort();
			} else if (format.equals("ICRD")) {
				this.creationDate = chunk.readString(chunk.available());
			} else if (format.equals("IENG")) {
				this.engineers = chunk.readString(chunk.available());
			} else if (format.equals("IPRD")) {
				this.product = chunk.readString(chunk.available());
			} else if (format.equals("ICOP")) {
				this.copyright = chunk.readString(chunk.available());
			} else if (format.equals("ICMT")) {
				this.comments = chunk.readString(chunk.available());
			} else if (format.equals("ISFT")) {
				this.tools = chunk.readString(chunk.available());
			}

		}
	}

	private void readSdtaChunk(RIFFReader riff) throws IOException {
		while (riff.hasNextChunk()) {
			RIFFReader chunk = riff.nextChunk();
			if (chunk.getFormat().equals("smpl")) {
				if (!largeFormat) {
					byte[] sampleData = new byte[chunk.available()];

					int read = 0;
					int avail = chunk.available();
					while (read != avail) {
						if (avail - read > 65536) {
							chunk.readFully(sampleData, read, 65536);
							read += 65536;
						} else {
							chunk.readFully(sampleData, read, avail - read);
							read = avail;
						}

					}
					this.sampleData = new ModelByteBuffer(sampleData);
					// chunk.read(sampleData);
				} else {
					this.sampleData = new ModelByteBuffer(sampleFile, chunk.getFilePointer(), chunk.available());
				}
			}
			if (chunk.getFormat().equals("sm24")) {
				if (!largeFormat) {
					byte[] sampleData24 = new byte[chunk.available()];
					// chunk.read(sampleData24);

					int read = 0;
					int avail = chunk.available();
					while (read != avail) {
						if (avail - read > 65536) {
							chunk.readFully(sampleData24, read, 65536);
							read += 65536;
						} else {
							chunk.readFully(sampleData24, read, avail - read);
							read = avail;
						}

					}
					this.sampleData24 = new ModelByteBuffer(sampleData24);
				} else {
					this.sampleData24 = new ModelByteBuffer(sampleFile, chunk.getFilePointer(), chunk.available());
				}

			}
		}
	}

	private void readPdtaChunk(RIFFReader riff) throws IOException {

		List<SF2Instrument> presets = new ArrayList<SF2Instrument>();
		List<Integer> presets_bagNdx = new ArrayList<Integer>();
		List<SF2InstrumentRegion> presets_splits_gen = new ArrayList<SF2InstrumentRegion>();
		List<SF2InstrumentRegion> presets_splits_mod = new ArrayList<SF2InstrumentRegion>();

		List<SF2Layer> instruments = new ArrayList<SF2Layer>();
		List<Integer> instruments_bagNdx = new ArrayList<Integer>();
		List<SF2LayerRegion> instruments_splits_gen = new ArrayList<SF2LayerRegion>();
		List<SF2LayerRegion> instruments_splits_mod = new ArrayList<SF2LayerRegion>();

		while (riff.hasNextChunk()) {
			RIFFReader chunk = riff.nextChunk();
			String format = chunk.getFormat();
			if (format.equals("phdr")) {
				// Preset Header / Instrument
				if (chunk.available() % 38 != 0)
					throw new RIFFInvalidDataException();
				int count = chunk.available() / 38;
				for (int i = 0; i < count; i++) {
					SF2Instrument preset = new SF2Instrument(this);
					preset.name = chunk.readString(20);
					preset.preset = chunk.readUnsignedShort();
					preset.bank = chunk.readUnsignedShort();
					presets_bagNdx.add(chunk.readUnsignedShort());
					preset.library = chunk.readUnsignedInt();
					preset.genre = chunk.readUnsignedInt();
					preset.morphology = chunk.readUnsignedInt();
					presets.add(preset);
					if (i != count - 1)
						this.instruments.add(preset);
				}
			} else if (format.equals("pbag")) {
				// Preset Zones / Instruments splits
				if (chunk.available() % 4 != 0)
					throw new RIFFInvalidDataException();
				int count = chunk.available() / 4;

				// Skip first record
				{
					int gencount = chunk.readUnsignedShort();
					int modcount = chunk.readUnsignedShort();
					while (presets_splits_gen.size() < gencount)
						presets_splits_gen.add(null);
					while (presets_splits_mod.size() < modcount)
						presets_splits_mod.add(null);
					count--;
				}

				int offset = presets_bagNdx.get(0);
				// Offset should be 0 (but just case)
				for (int i = 0; i < offset; i++) {
					if (count == 0)
						throw new RIFFInvalidDataException();
					int gencount = chunk.readUnsignedShort();
					int modcount = chunk.readUnsignedShort();
					while (presets_splits_gen.size() < gencount)
						presets_splits_gen.add(null);
					while (presets_splits_mod.size() < modcount)
						presets_splits_mod.add(null);
					count--;
				}

				for (int i = 0; i < presets_bagNdx.size() - 1; i++) {
					int zone_count = presets_bagNdx.get(i + 1) - presets_bagNdx.get(i);
					SF2Instrument preset = presets.get(i);
					for (int ii = 0; ii < zone_count; ii++) {
						if (count == 0)
							throw new RIFFInvalidDataException();
						int gencount = chunk.readUnsignedShort();
						int modcount = chunk.readUnsignedShort();
						SF2InstrumentRegion split = new SF2InstrumentRegion();
						preset.regions.add(split);
						while (presets_splits_gen.size() < gencount)
							presets_splits_gen.add(split);
						while (presets_splits_mod.size() < modcount)
							presets_splits_mod.add(split);
						count--;
					}
				}
			} else if (format.equals("pmod")) {
				// Preset Modulators / Split Modulators
				for (int i = 0; i < presets_splits_mod.size(); i++) {
					SF2Modulator modulator = new SF2Modulator();
					modulator.sourceOperator = chunk.readUnsignedShort();
					modulator.destinationOperator = chunk.readUnsignedShort();
					modulator.amount = chunk.readShort();
					modulator.amountSourceOperator = chunk.readUnsignedShort();
					modulator.transportOperator = chunk.readUnsignedShort();
					SF2InstrumentRegion split = presets_splits_mod.get(i);
					if (split != null)
						split.modulators.add(modulator);
				}
			} else if (format.equals("pgen")) {
				// Preset Generators / Split Generators
				for (int i = 0; i < presets_splits_gen.size(); i++) {
					int operator = chunk.readUnsignedShort();
					short amount = chunk.readShort();
					SF2InstrumentRegion split = presets_splits_gen.get(i);
					if (split != null)
						split.generators.put(operator, amount);
				}
			} else if (format.equals("inst")) {
				// Instrument Header / Layers
				if (chunk.available() % 22 != 0)
					throw new RIFFInvalidDataException();
				int count = chunk.available() / 22;
				for (int i = 0; i < count; i++) {
					SF2Layer layer = new SF2Layer(this);
					layer.name = chunk.readString(20);
					instruments_bagNdx.add(chunk.readUnsignedShort());
					instruments.add(layer);
					if (i != count - 1)
						this.layers.add(layer);
				}
			} else if (format.equals("ibag")) {
				// Instrument Zones / Layer splits
				if (chunk.available() % 4 != 0)
					throw new RIFFInvalidDataException();
				int count = chunk.available() / 4;

				// Skip first record
				{
					int gencount = chunk.readUnsignedShort();
					int modcount = chunk.readUnsignedShort();
					while (instruments_splits_gen.size() < gencount)
						instruments_splits_gen.add(null);
					while (instruments_splits_mod.size() < modcount)
						instruments_splits_mod.add(null);
					count--;
				}

				int offset = instruments_bagNdx.get(0);
				// Offset should be 0 (but just case)
				for (int i = 0; i < offset; i++) {
					if (count == 0)
						throw new RIFFInvalidDataException();
					int gencount = chunk.readUnsignedShort();
					int modcount = chunk.readUnsignedShort();
					while (instruments_splits_gen.size() < gencount)
						instruments_splits_gen.add(null);
					while (instruments_splits_mod.size() < modcount)
						instruments_splits_mod.add(null);
					count--;
				}

				for (int i = 0; i < instruments_bagNdx.size() - 1; i++) {
					int zone_count = instruments_bagNdx.get(i + 1) - instruments_bagNdx.get(i);
					SF2Layer layer = layers.get(i);
					for (int ii = 0; ii < zone_count; ii++) {
						if (count == 0)
							throw new RIFFInvalidDataException();
						int gencount = chunk.readUnsignedShort();
						int modcount = chunk.readUnsignedShort();
						SF2LayerRegion split = new SF2LayerRegion();
						layer.regions.add(split);
						while (instruments_splits_gen.size() < gencount)
							instruments_splits_gen.add(split);
						while (instruments_splits_mod.size() < modcount)
							instruments_splits_mod.add(split);
						count--;
					}
				}

			} else if (format.equals("imod")) {
				// Instrument Modulators / Split Modulators
				for (int i = 0; i < instruments_splits_mod.size(); i++) {
					SF2Modulator modulator = new SF2Modulator();
					modulator.sourceOperator = chunk.readUnsignedShort();
					modulator.destinationOperator = chunk.readUnsignedShort();
					modulator.amount = chunk.readShort();
					modulator.amountSourceOperator = chunk.readUnsignedShort();
					modulator.transportOperator = chunk.readUnsignedShort();
					SF2LayerRegion split = instruments_splits_gen.get(i);
					if (split != null)
						split.modulators.add(modulator);
				}
			} else if (format.equals("igen")) {
				// Instrument Generators / Split Generators
				for (int i = 0; i < instruments_splits_gen.size(); i++) {
					int operator = chunk.readUnsignedShort();
					short amount = chunk.readShort();
					SF2LayerRegion split = instruments_splits_gen.get(i);
					if (split != null)
						split.generators.put(operator, amount);
				}
			} else if (format.equals("shdr")) {
				// Sample Headers
				if (chunk.available() % 46 != 0)
					throw new RIFFInvalidDataException();
				int count = chunk.available() / 46;
				for (int i = 0; i < count; i++) {
					SF2Sample sample = new SF2Sample(this);
					sample.name = chunk.readString(20);
					long start = chunk.readUnsignedInt();
					long end = chunk.readUnsignedInt();
					sample.data = sampleData.subbuffer(start * 2, end * 2, true);
					if (sampleData24 != null)
						sample.data24 = sampleData24.subbuffer(start, end, true);
					/*
					 * sample.data = new ModelByteBuffer(sampleData, (int)(start*2), (int)((end - start)*2)); if (sampleData24 != null) sample.data24 = new ModelByteBuffer(sampleData24, (int)start,
					 * (int)(end - start));
					 */
					sample.startLoop = chunk.readUnsignedInt() - start;
					sample.endLoop = chunk.readUnsignedInt() - start;
					if (sample.startLoop < 0)
						sample.startLoop = -1;
					if (sample.endLoop < 0)
						sample.endLoop = -1;
					sample.sampleRate = chunk.readUnsignedInt();
					sample.originalPitch = chunk.readUnsignedByte();
					sample.pitchCorrection = chunk.readByte();
					sample.sampleLink = chunk.readUnsignedShort();
					sample.sampleType = chunk.readUnsignedShort();
					if (i != count - 1)
						this.samples.add(sample);
				}
			}
		}

		Iterator<SF2Layer> liter = this.layers.iterator();
		while (liter.hasNext()) {
			SF2Layer layer = liter.next();
			Iterator<SF2LayerRegion> siter = layer.regions.iterator();
			SF2Region globalsplit = null;
			while (siter.hasNext()) {
				SF2LayerRegion split = siter.next();
				if (split.generators.get(SF2LayerRegion.GENERATOR_SAMPLEID) != null) {
					int sampleid = split.generators.get(SF2LayerRegion.GENERATOR_SAMPLEID);
					split.generators.remove(SF2LayerRegion.GENERATOR_SAMPLEID);
					split.sample = samples.get(sampleid);
				} else {
					globalsplit = split;
				}
			}
			if (globalsplit != null) {
				layer.getRegions().remove(globalsplit);
				SF2GlobalRegion gsplit = new SF2GlobalRegion();
				gsplit.generators = globalsplit.generators;
				gsplit.modulators = globalsplit.modulators;
				layer.setGlobalZone(gsplit);
			}
		}

		Iterator<SF2Instrument> iiter = this.instruments.iterator();
		while (iiter.hasNext()) {
			SF2Instrument instrument = iiter.next();
			Iterator<SF2InstrumentRegion> siter = instrument.regions.iterator();
			SF2Region globalsplit = null;
			while (siter.hasNext()) {
				SF2InstrumentRegion split = siter.next();
				if (split.generators.get(SF2LayerRegion.GENERATOR_INSTRUMENT) != null) {
					int instrumentid = split.generators.get(SF2InstrumentRegion.GENERATOR_INSTRUMENT);
					split.generators.remove(SF2LayerRegion.GENERATOR_INSTRUMENT);
					split.layer = layers.get(instrumentid);
				} else {
					globalsplit = split;
				}
			}

			if (globalsplit != null) {
				instrument.getRegions().remove(globalsplit);
				SF2GlobalRegion gsplit = new SF2GlobalRegion();
				gsplit.generators = globalsplit.generators;
				gsplit.modulators = globalsplit.modulators;
				instrument.setGlobalZone(gsplit);
			}
		}

	}

	public void save(String name) throws IOException {
		writeSoundbank(new RIFFWriter(name, "sfbk"));
	}

	public void save(File file) throws IOException {
		writeSoundbank(new RIFFWriter(file, "sfbk"));
	}

	public void save(OutputStream out) throws IOException {
		writeSoundbank(new RIFFWriter(out, "sfbk"));
	}

	private void writeSoundbank(RIFFWriter writer) throws IOException {
		writeInfo(writer.writeList("INFO"));
		writeSdtaChunk(writer.writeList("sdta"));
		writePdtaChunk(writer.writeList("pdta"));
		writer.close();
	}

	private void writeInfoStringChunk(RIFFWriter writer, String name, String value) throws IOException {
		if (value == null)
			return;
		RIFFWriter chunk = writer.writeChunk(name);
		chunk.writeString(value);
		int len = value.getBytes("ascii").length;
		chunk.write(0);
		len++;
		if (len % 2 != 0)
			chunk.write(0);
	}

	private void writeInfo(RIFFWriter writer) throws IOException {
		if (this.targetEngine == null)
			this.targetEngine = "EMU8000";
		if (this.name == null)
			this.name = "";

		RIFFWriter ifil_chunk = writer.writeChunk("ifil");
		ifil_chunk.writeUnsignedShort(this.major);
		ifil_chunk.writeUnsignedShort(this.minor);
		writeInfoStringChunk(writer, "isng", this.targetEngine);
		writeInfoStringChunk(writer, "INAM", this.name);
		writeInfoStringChunk(writer, "irom", this.romName);
		if (romVersionMajor != -1) {
			RIFFWriter iver_chunk = writer.writeChunk("iver");
			iver_chunk.writeUnsignedShort(this.romVersionMajor);
			iver_chunk.writeUnsignedShort(this.romVersionMinor);
		}
		writeInfoStringChunk(writer, "ICRD", this.creationDate);
		writeInfoStringChunk(writer, "IENG", this.engineers);
		writeInfoStringChunk(writer, "IPRD", this.product);
		writeInfoStringChunk(writer, "ICOP", this.copyright);
		writeInfoStringChunk(writer, "ICMT", this.comments);
		writeInfoStringChunk(writer, "ISFT", this.tools);

		writer.close();
	}

	private void writeSdtaChunk(RIFFWriter writer) throws IOException {

		byte[] pad = new byte[32];

		RIFFWriter smpl_chunk = writer.writeChunk("smpl");
		for (SF2Sample sample : samples) {
			ModelByteBuffer data = sample.getDataBuffer();
			data.writeTo(smpl_chunk);
			/*
			 * smpl_chunk.write(data.array(), data.arrayOffset(), data.capacity());
			 */
			smpl_chunk.write(pad);
			smpl_chunk.write(pad);
		}
		if (major < 2)
			return;
		if (major == 2 && minor < 4)
			return;

		for (SF2Sample sample : samples) {
			ModelByteBuffer data24 = sample.getData24Buffer();
			if (data24 == null)
				return;
		}

		RIFFWriter sm24_chunk = writer.writeChunk("sm24");
		for (SF2Sample sample : samples) {
			ModelByteBuffer data = sample.getData24Buffer();
			data.writeTo(sm24_chunk);
			/*
			 * sm24_chunk.write(data.array(), data.arrayOffset(), data.capacity());
			 */
			smpl_chunk.write(pad);
		}
	}

	private void writeModulators(RIFFWriter writer, List<SF2Modulator> modulators) throws IOException {
		for (SF2Modulator modulator : modulators) {
			writer.writeUnsignedShort(modulator.sourceOperator);
			writer.writeUnsignedShort(modulator.destinationOperator);
			writer.writeShort(modulator.amount);
			writer.writeUnsignedShort(modulator.amountSourceOperator);
			writer.writeUnsignedShort(modulator.transportOperator);
		}
	}

	private void writeGenerators(RIFFWriter writer, Map<Integer, Short> generators) throws IOException {
		Short keyrange = (Short) generators.get(SF2Region.GENERATOR_KEYRANGE);
		Short velrange = (Short) generators.get(SF2Region.GENERATOR_VELRANGE);
		if (keyrange != null) {
			writer.writeUnsignedShort(SF2Region.GENERATOR_KEYRANGE);
			writer.writeShort(keyrange);
		}
		if (velrange != null) {
			writer.writeUnsignedShort(SF2Region.GENERATOR_VELRANGE);
			writer.writeShort(velrange);
		}
		for (Map.Entry<Integer, Short> generator : generators.entrySet()) {
			if (generator.getKey() == SF2Region.GENERATOR_KEYRANGE)
				continue;
			if (generator.getKey() == SF2Region.GENERATOR_VELRANGE)
				continue;
			writer.writeUnsignedShort(generator.getKey());
			writer.writeShort(generator.getValue());
		}
	}

	private void writePdtaChunk(RIFFWriter writer) throws IOException {

		RIFFWriter phdr_chunk = writer.writeChunk("phdr");
		int phdr_zone_count = 0;
		for (SF2Instrument preset : this.instruments) {
			phdr_chunk.writeString(preset.name, 20);
			phdr_chunk.writeUnsignedShort(preset.preset);
			phdr_chunk.writeUnsignedShort(preset.bank);
			phdr_chunk.writeUnsignedShort(phdr_zone_count);
			if (preset.getGlobalRegion() != null)
				phdr_zone_count += 1;
			phdr_zone_count += preset.getRegions().size();
			phdr_chunk.writeUnsignedInt(preset.library);
			phdr_chunk.writeUnsignedInt(preset.genre);
			phdr_chunk.writeUnsignedInt(preset.morphology);
		}
		phdr_chunk.writeString("EOP", 20);
		phdr_chunk.writeUnsignedShort(0);
		phdr_chunk.writeUnsignedShort(0);
		phdr_chunk.writeUnsignedShort(phdr_zone_count);
		phdr_chunk.writeUnsignedInt(0);
		phdr_chunk.writeUnsignedInt(0);
		phdr_chunk.writeUnsignedInt(0);

		RIFFWriter pbag_chunk = writer.writeChunk("pbag");
		int pbag_gencount = 0;
		int pbag_modcount = 0;
		for (SF2Instrument preset : this.instruments) {
			if (preset.getGlobalRegion() != null) {
				pbag_chunk.writeUnsignedShort(pbag_gencount);
				pbag_chunk.writeUnsignedShort(pbag_modcount);
				pbag_gencount += preset.getGlobalRegion().getGenerators().size();
				pbag_modcount += preset.getGlobalRegion().getModulators().size();
			}
			for (SF2InstrumentRegion region : preset.getRegions()) {
				pbag_chunk.writeUnsignedShort(pbag_gencount);
				pbag_chunk.writeUnsignedShort(pbag_modcount);
				if (layers.indexOf(region.layer) != -1) {
					// One generator is used to reference to instrument record
					pbag_gencount += 1;
				}
				pbag_gencount += region.getGenerators().size();
				pbag_modcount += region.getModulators().size();

			}
		}
		pbag_chunk.writeUnsignedShort(pbag_gencount);
		pbag_chunk.writeUnsignedShort(pbag_modcount);

		RIFFWriter pmod_chunk = writer.writeChunk("pmod");
		for (SF2Instrument preset : this.instruments) {
			if (preset.getGlobalRegion() != null) {
				writeModulators(pmod_chunk, preset.getGlobalRegion().getModulators());
			}
			for (SF2InstrumentRegion region : preset.getRegions())
				writeModulators(pmod_chunk, region.getModulators());
		}
		pmod_chunk.write(new byte[10]);

		RIFFWriter pgen_chunk = writer.writeChunk("pgen");
		for (SF2Instrument preset : this.instruments) {
			if (preset.getGlobalRegion() != null) {
				writeGenerators(pgen_chunk, preset.getGlobalRegion().getGenerators());
			}
			for (SF2InstrumentRegion region : preset.getRegions()) {
				writeGenerators(pgen_chunk, region.getGenerators());
				int ix = (int) layers.indexOf(region.layer);
				if (ix != -1) {
					pgen_chunk.writeUnsignedShort(SF2Region.GENERATOR_INSTRUMENT);
					pgen_chunk.writeShort((short) ix);
				}
			}
		}
		pgen_chunk.write(new byte[4]);

		RIFFWriter inst_chunk = writer.writeChunk("inst");
		int inst_zone_count = 0;
		for (SF2Layer instrument : this.layers) {
			inst_chunk.writeString(instrument.name, 20);
			inst_chunk.writeUnsignedShort(inst_zone_count);
			if (instrument.getGlobalRegion() != null)
				inst_zone_count += 1;
			inst_zone_count += instrument.getRegions().size();
		}
		inst_chunk.writeString("EOI", 20);
		inst_chunk.writeUnsignedShort(inst_zone_count);

		RIFFWriter ibag_chunk = writer.writeChunk("ibag");
		int ibag_gencount = 0;
		int ibag_modcount = 0;
		for (SF2Layer instrument : this.layers) {
			if (instrument.getGlobalRegion() != null) {
				ibag_chunk.writeUnsignedShort(ibag_gencount);
				ibag_chunk.writeUnsignedShort(ibag_modcount);
				ibag_gencount += instrument.getGlobalRegion().getGenerators().size();
				ibag_modcount += instrument.getGlobalRegion().getModulators().size();
			}
			for (SF2LayerRegion region : instrument.getRegions()) {
				ibag_chunk.writeUnsignedShort(ibag_gencount);
				ibag_chunk.writeUnsignedShort(ibag_modcount);
				if (samples.indexOf(region.sample) != -1) {
					// One generator is used to reference to instrument record
					ibag_gencount += 1;
				}
				ibag_gencount += region.getGenerators().size();
				ibag_modcount += region.getModulators().size();

			}
		}
		ibag_chunk.writeUnsignedShort(ibag_gencount);
		ibag_chunk.writeUnsignedShort(ibag_modcount);

		RIFFWriter imod_chunk = writer.writeChunk("imod");
		for (SF2Layer instrument : this.layers) {
			if (instrument.getGlobalRegion() != null) {
				writeModulators(imod_chunk, instrument.getGlobalRegion().getModulators());
			}
			for (SF2LayerRegion region : instrument.getRegions())
				writeModulators(imod_chunk, region.getModulators());
		}
		imod_chunk.write(new byte[10]);

		RIFFWriter igen_chunk = writer.writeChunk("igen");
		for (SF2Layer instrument : this.layers) {
			if (instrument.getGlobalRegion() != null) {
				writeGenerators(igen_chunk, instrument.getGlobalRegion().getGenerators());
			}
			for (SF2LayerRegion region : instrument.getRegions()) {
				writeGenerators(igen_chunk, region.getGenerators());
				int ix = samples.indexOf(region.sample);
				if (ix != -1) {
					igen_chunk.writeUnsignedShort(SF2Region.GENERATOR_SAMPLEID);
					igen_chunk.writeShort((short) ix);
				}
			}
		}
		igen_chunk.write(new byte[4]);

		RIFFWriter shdr_chunk = writer.writeChunk("shdr");
		long sample_pos = 0;
		for (SF2Sample sample : samples) {
			shdr_chunk.writeString(sample.name, 20);
			long start = sample_pos;
			sample_pos += sample.data.capacity() / 2;
			long end = sample_pos;
			long startLoop = sample.startLoop + start;
			long endLoop = sample.endLoop + start;
			if (startLoop < start)
				startLoop = start;
			if (endLoop > end)
				endLoop = end;
			shdr_chunk.writeUnsignedInt(start);
			shdr_chunk.writeUnsignedInt(end);
			shdr_chunk.writeUnsignedInt(startLoop);
			shdr_chunk.writeUnsignedInt(endLoop);
			shdr_chunk.writeUnsignedInt(sample.sampleRate);
			shdr_chunk.writeUnsignedByte(sample.originalPitch);
			shdr_chunk.writeByte(sample.pitchCorrection);
			shdr_chunk.writeUnsignedShort(sample.sampleLink);
			shdr_chunk.writeUnsignedShort(sample.sampleType);
			sample_pos += 32;
		}
		shdr_chunk.writeString("EOS", 20);
		shdr_chunk.write(new byte[26]);

	}

	public String getName() {
		return name;
	}

	public String getVersion() {
		return major + "." + minor;
	}

	public String getVendor() {
		return engineers;
	}

	public String getDescription() {
		return comments;
	}

	public void setName(String s) {
		name = s;
	}

	public void setVendor(String s) {
		engineers = s;
	}

	public void setDescription(String s) {
		comments = s;
	}

	public SoundbankResource[] getResources() {
		SoundbankResource[] resources = new SoundbankResource[layers.size() + samples.size()];
		int j = 0;
		for (int i = 0; i < layers.size(); i++)
			resources[j++] = layers.get(i);
		for (int i = 0; i < samples.size(); i++)
			resources[j++] = samples.get(i);
		return resources;
	}

	public SF2Instrument[] getInstruments() {
		SF2Instrument[] inslist_array = instruments.toArray(new SF2Instrument[instruments.size()]);
		Arrays.sort(inslist_array, new ModelInstrumentComparator());
		return inslist_array;
	}

	public SF2Layer[] getLayers() {
		return layers.toArray(new SF2Layer[layers.size()]);
	}

	public SF2Sample[] getSamples() {
		return samples.toArray(new SF2Sample[samples.size()]);
	}

	public Instrument getInstrument(Patch patch) {
		int program = patch.getProgram();
		int bank = patch.getBank();
		boolean percussion = false;
		if (patch instanceof ModelPatch)
			percussion = ((ModelPatch) patch).isPercussion();
		for (Instrument instrument : instruments) {
			Patch patch2 = instrument.getPatch();
			int program2 = patch2.getProgram();
			int bank2 = patch2.getBank();
			if (program == program2 && bank == bank2) {
				boolean percussion2 = false;
				if (patch2 instanceof ModelPatch)
					percussion2 = ((ModelPatch) patch2).isPercussion();
				if (percussion == percussion2)
					return instrument;
			}
		}
		return null;
	}

	public String getCreationDate() {
		return creationDate;
	}

	public void setCreationDate(String creationDate) {
		this.creationDate = creationDate;
	}

	public String getProduct() {
		return product;
	}

	public void setProduct(String product) {
		this.product = product;
	}

	public String getRomName() {
		return romName;
	}

	public void setRomName(String romName) {
		this.romName = romName;
	}

	public int getRomVersionMajor() {
		return romVersionMajor;
	}

	public void setRomVersionMajor(int romVersionMajor) {
		this.romVersionMajor = romVersionMajor;
	}

	public int getRomVersionMinor() {
		return romVersionMinor;
	}

	public void setRomVersionMinor(int romVersionMinor) {
		this.romVersionMinor = romVersionMinor;
	}

	public String getTargetEngine() {
		return targetEngine;
	}

	public void setTargetEngine(String targetEngine) {
		this.targetEngine = targetEngine;
	}

	public String getTools() {
		return tools;
	}

	public void setTools(String tools) {
		this.tools = tools;
	}

	public void addResource(SoundbankResource resource) {
		if (resource instanceof SF2Instrument)
			instruments.add((SF2Instrument) resource);
		if (resource instanceof SF2Layer)
			layers.add((SF2Layer) resource);
		if (resource instanceof SF2Sample)
			samples.add((SF2Sample) resource);
	}

	public void removeResource(SoundbankResource resource) {
		if (resource instanceof SF2Instrument)
			instruments.remove((SF2Instrument) resource);
		if (resource instanceof SF2Layer)
			layers.remove((SF2Layer) resource);
		if (resource instanceof SF2Sample)
			samples.remove((SF2Sample) resource);
	}

	public void addInstrument(SF2Instrument resource) {
		instruments.add(resource);
	}

	public void removeInstrument(SF2Instrument resource) {
		instruments.remove(resource);
	}
}
